﻿#region Using Statements
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Graphics.PackedVector;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Text.RegularExpressions;
#endregion

namespace ContentBase.Models
{
    /// <summary>
    /// Processes a model adding support for custom effects such as normal, parallax, specular and emissive mapping.
    /// </summary>
    [ContentProcessor(DisplayName = "Custom Effect Model - ContentBase")]
    public class CustomEffectModelProcessor : ModelProcessor
    {
        #region Fields

        // folder where the processed model is located
        string modelDirectory;

        #endregion

        #region Tangent Frames

        /// <summary>
        /// Forces generation of tangent frames, required for normal and specular mapping.
        /// </summary>
        [Browsable(false)]
        public override bool GenerateTangentFrames
        {
            get { return true; }
            set { }
        }

        #endregion

        #region Custom Effect

        // backing fields
        string customEffect;

        /// <summary>
        /// Gets or sets the custom effect used for rendering.
        /// </summary>
        [DisplayName("Custom Effect")]
        [Description("Specifies a custom effect used for rendering the model. If not set explicitly, the effect specified as default will be used instead.")]
        [DefaultValue("")]
        public string CustomEffect
        {
            get { return customEffect; }
            set { customEffect = value; }
        }

        #endregion

        #region Parallax Mapping

        // backing fields
        float parallaxMapScale;
        float parallaxMapBias;

        /// <summary>
        /// Specifies the parallax mapping scale factor.
        /// </summary>
        [DisplayName("Parallax Mapping Scale")]
        [Description("Specifies the parallax mapping scale factor.")]
        [DefaultValue(0)]
        public float ParallaxMapScale
        {
            get { return parallaxMapScale; }
            set { parallaxMapScale = value; }
        }

        /// <summary>
        /// Specifies the parallax mapping bias factor.
        /// </summary>
        [DisplayName("Parallax Mapping Bias")]
        [Description("Specifies the parallax mapping bias factor.")]
        [DefaultValue(0)]
        public float ParallaxMapBias
        {
            get { return parallaxMapBias; }
            set { parallaxMapBias = value; }
        }

        #endregion

        #region Process

        /// <summary>
        /// Converts mesh content to model content. 
        /// </summary>
        /// <param name="input">The root node content.</param>
        /// <param name="context">Context for the specified processor.</param>
        /// <returns>The model content.</returns>
        public override ModelContent Process(NodeContent input, ContentProcessorContext context)
        {
            // no custom effect specified - use default
            if (!string.IsNullOrEmpty(customEffect))
            {
                modelDirectory = Path.GetDirectoryName(input.Identity.SourceFilename);
                LookUpTextures(input, context);
            }

            return base.Process(input, context);
        }

        #endregion

        #region Textures
                
        /// <summary>
        /// Looks for all the texture maps associated to the model.
        /// <para>Those will be added to the textures collection of every of the mesh's materials.</para>
        /// </summary>
        /// <param name="node">The node content to be processed.</param>
        /// <param name="context">Context for the specified processor.</param>
        private void LookUpTextures(NodeContent node, ContentProcessorContext context)
        {
            MeshContent mesh = node as MeshContent;

            if (mesh != null)
            {
                #region Textures

                foreach (GeometryContent geometry in mesh.Geometry)
                {
                    ExternalReference<TextureContent> diffuseTexture;

                    if (geometry.Material.Textures.TryGetValue("Texture", out diffuseTexture))
                    {
                        // diffuse map texture found... look up remaining texture maps

                        string texDir = Path.GetDirectoryName(diffuseTexture.Filename);
                        string texExt = Path.GetExtension(diffuseTexture.Filename);
                        string texFile = Path.GetFileNameWithoutExtension(diffuseTexture.Filename);

                        // obtain the base name for all texture maps
                        Regex regex = new Regex(@"_DIFFUSE$");
                        string[] substrings = regex.Split(texFile);
                        texFile = substrings[0];
                                                
                        #region Normal Mapping

                        string normalMapFile = texDir + @"\" + texFile + "_NORMAL" + texExt;
                        
                        if (File.Exists(normalMapFile))
                        {
                            geometry.Material.Textures.Add("NormalMap", new ExternalReference<TextureContent>(normalMapFile));
                        }                     

                        #endregion

                        #region Parallax Mapping

                        string dispMapFile = texDir + @"\" + texFile + "_DISP" + texExt;

                        if (File.Exists(dispMapFile))
                        {
                            geometry.Material.Textures.Add("DispMap", new ExternalReference<TextureContent>(dispMapFile));
                        }

                        #endregion

                        #region Specular Mapping

                        string specularMapFile = texDir + @"\" + texFile + "_SPECULAR" + texExt;

                        if (File.Exists(specularMapFile))
                        {
                            geometry.Material.Textures.Add("SpecularMap", new ExternalReference<TextureContent>(specularMapFile));
                        }

                        #endregion

                        #region Emissive Mapping

                        string emissiveMapFile = texDir + @"\" + texFile + "_GLOW" + texExt;

                        if (File.Exists(emissiveMapFile))
                        {
                            geometry.Material.Textures.Add("EmissiveMap", new ExternalReference<TextureContent>(emissiveMapFile));
                        }

                        #endregion
                    }
                }

                #endregion
            }

            foreach (NodeContent child in node.Children)
                LookUpTextures(child, context);
        }

        #endregion

        #region Vertex Channels

        // vertex channels used for normal and specular mapping...
        // ... remove all other vertex channels
        readonly IList<string> usedVertexChannels = new string[]
        {
                VertexChannelNames.TextureCoordinate(0),
                VertexChannelNames.Normal(0),
                VertexChannelNames.Binormal(0),
                VertexChannelNames.Tangent(0)
        };

        /// <summary>
        /// Processes geometry content vertex channels at the specified index.
        /// </summary>
        /// <param name="geometry">The geometry content to process.</param>
        /// <param name="vertexChannelIndex">Index of the vertex channel to process.</param>
        /// <param name="context">Context for the specified processor.</param>
        protected override void ProcessVertexChannel(GeometryContent geometry, int vertexChannelIndex, ContentProcessorContext context)
        {
            string vertexChannelName = geometry.Vertices.Channels[vertexChannelIndex].Name;

            if (usedVertexChannels.Contains(vertexChannelName))
                // vertex channel used for normal and specular mapping... process it as usual
                base.ProcessVertexChannel(geometry, vertexChannelIndex, context);
            else
                // vertex channel not used... remove it
                geometry.Vertices.Channels.Remove(vertexChannelName);
        }

        #endregion

        #region Materials

        /// <summary>
        /// Makes the model use the specified custom effect, loading any texture relevant for rendering.
        /// </summary>
        /// <param name="material">The input material content.</param>
        /// <param name="context">Context for the specified processor.</param>
        /// <returns>The converted material content.</returns>
        protected override MaterialContent ConvertMaterial(MaterialContent material, ContentProcessorContext context)
        {
            // no custom effect specified - use default
            if (string.IsNullOrEmpty(customEffect))
                return base.ConvertMaterial(material, context);

            // load custom effect
            EffectMaterialContent customEffectMaterial = new EffectMaterialContent();
            customEffectMaterial.Effect = new ExternalReference<EffectContent>(customEffect);

            // load relevant textures...
            // ... the method LookUpTextures added normal, displacement, specular and emissive map textures to the MaterialContent.Textures collection
            foreach (KeyValuePair<string, ExternalReference<TextureContent>> texture in material.Textures)
            {
                switch (texture.Key)
                {
                    case "Texture":
                        {
                            customEffectMaterial.Textures.Add("DiffuseMap", texture.Value);
                            break;
                        }
                    case "NormalMap":
                    case "DispMap":
                    case "SpecularMap":
                    case "EmissiveMap":
                        {
                            customEffectMaterial.Textures.Add(texture.Key, texture.Value);
                            break;
                        }               
                    default:
                        break;
                }
            }

            // parallax mapping
            customEffectMaterial.OpaqueData.Add("ParallaxMapScale", parallaxMapScale);
            customEffectMaterial.OpaqueData.Add("ParallaxMapBias", parallaxMapBias);

            // copy processor parameters
            OpaqueDataDictionary parameters = new OpaqueDataDictionary();
            parameters["ColorKeyColor"] = this.ColorKeyColor;
            parameters["ColorKeyEnabled"] = this.ColorKeyEnabled;
            parameters["GenerateMipmaps"] = this.GenerateMipmaps;
            parameters["PremultiplyTextureAlpha"] = this.PremultiplyTextureAlpha;
            parameters["ResizeTexturesToPowerOfTwo"] = this.ResizeTexturesToPowerOfTwo;
            parameters["TextureFormat"] = this.TextureFormat;

            return context.Convert<MaterialContent, MaterialContent>(customEffectMaterial, typeof(CustomEffectMaterialProcessor).Name, parameters);
        }

        #endregion
    }

    #region Material Processor

    /// <summary>
    /// Provides a custom material processor which allows specification of custom processing of the effects and textures of a MaterialContent object.
    /// </summary>
    [ContentProcessor]
    [DesignTimeVisible(false)]
    internal class CustomEffectMaterialProcessor : MaterialProcessor
    {
        /// <summary>
        /// Builds texture content.
        /// </summary>
        /// <param name="textureName">The name of the texture. This should correspond to the key used to store the texture in Textures.
        /// <para>It can be used to determine which processor to use.</para></param>
        /// <param name="texture">The asset to build. This should be a member of Textures.</param>
        /// <param name="context">Context for the specified processor.</param>
        /// <returns>The built texture content.</returns>
        protected override ExternalReference<TextureContent> BuildTexture(string textureName, ExternalReference<TextureContent> texture, ContentProcessorContext context)
        {
            if (textureName == "NormalMap")
            {
                // for the normal map, use NormalMapTextureProcessor
                return context.BuildAsset<TextureContent, TextureContent>(texture, typeof(NormalMapTextureProcessor).Name);
            }

            // default processing to all other textures
            return base.BuildTexture(textureName, texture, context);
        }
    }

    #endregion

    #region Texture Processor

    /// <summary>
    /// Provides a custom texture processor which allows custom processing of normal maps.
    /// <para>It transforms the textures with the encoded normals from unsigned format [0,1] to signed format [-1,1].</para>
    /// </summary>
    [ContentProcessor]
    [DesignTimeVisible(false)]
    internal class NormalMapTextureProcessor : ContentProcessor<TextureContent, TextureContent>
    {
        /// <summary>
        /// Processes the specified input data and returns the result.
        /// </summary>
        /// <param name="input">Existing content object being processed.</param>
        /// <param name="context">Contains any required custom process parameters.</param>
        /// <returns>A typed object representing the processed input.</returns>
        public override TextureContent Process(TextureContent input, ContentProcessorContext context)
        {
            input.ConvertBitmapType(typeof(PixelBitmapContent<Vector4>));

            foreach (MipmapChain mipmapChain in input.Faces)
            {
                foreach (PixelBitmapContent<Vector4> bitmap in mipmapChain)
                {
                    for (int x = 0; x < bitmap.Width; x++)
                    {
                        for (int y = 0; y < bitmap.Height; y++)
                        {
                            Vector4 pxl = bitmap.GetPixel(x, y);
                            Vector3 pxl_rgb = new Vector3(pxl.X, pxl.Y, pxl.Z);
                            pxl_rgb = 2 * pxl_rgb - Vector3.One;
                            bitmap.SetPixel(x, y, new Vector4(pxl_rgb, pxl.W));
                        }
                    }
                }
            }

            input.ConvertBitmapType(typeof(PixelBitmapContent<NormalizedByte4>));

            return input;
        }
    }

    #endregion
}
